package br.org.acessobrasil.commons;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;

/**
 * Contem rotinas basicas ao manipular um XMl 
 *
 */
public class Xml {
	public Document dom;
	private Node node;
	private static Logger logger = Logger.getLogger(Xml.class);
	private static DocumentBuilderFactory domFactory;
	public static long totalTempoById = 0 ; 
	private HashMap<String,Node> indicePorId = null;
	
	static{
		domFactory = DocumentBuilderFactory.newInstance();
		domFactory.setValidating(false);
	}
	
	protected Xml(){}
	
	public Document getDom() {
		return dom;
	}
	
	public void setDom(Document dom) {
		this.dom = dom;
	}
	
	public Xml(String fullPath) {
		this.open(fullPath);
	}

	public Xml(Document dom) {
		this.dom = dom;
	}
	
	public Xml(Node node) {
		this.node = node;
	}
	
	public void open(String fullPath) {
		DocumentBuilder domBuilder;
		try {
			domBuilder = domFactory.newDocumentBuilder();
			
			InputSource myXML = getXML(fullPath);
			if (myXML != null)
				this.dom = domBuilder.parse(myXML);			
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Avalia o DTD
	 * @param file
	 * @return Document
	 * @throws ParserConfigurationException 
	 * @throws IOException 
	 * @throws SAXException 
	 */
	public static Document parseAvaliador(File file) throws ParserConfigurationException, SAXException, IOException{
		DocumentBuilder domBuilder;
		domFactory.setValidating(true);
		domBuilder = domFactory.newDocumentBuilder();
		InputSource myXML = getXML(file);
		Document doc = domBuilder.parse(myXML);
		domFactory.setValidating(false);
		if (myXML != null)
			return doc;
		return null;
	}
	
	/**
	 * Nao avalia o DTD
	 * @param file
	 * @return Document
	 * @throws ParserConfigurationException 
	 * @throws IOException 
	 * @throws SAXException 
	 */
	public static Document parse(File file) throws ParserConfigurationException, SAXException, IOException{
		DocumentBuilder domBuilder;
		domBuilder = domFactory.newDocumentBuilder();
		InputSource myXML = getXML(file);
		if (myXML != null)
			return domBuilder.parse(myXML);
		return null;
	}
	
	/**
	 * Dar parse em uma String para XML Validando com DocType
	 * @param codigoXml
	 * @return Document
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 */
	public static Document parseAvaliador(String codigoXml) throws ParserConfigurationException, SAXException, IOException {
		//Retirar sujeira ﻿<?xml
		int ini =codigoXml.indexOf("<?xml"); 
		if(ini>0){
			codigoXml = codigoXml.substring(ini);
		}
		
		DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
		domFactory.setValidating(true);
		DocumentBuilder domBuilder = domFactory.newDocumentBuilder();
		InputSource inStream = new InputSource();
		inStream.setCharacterStream(new StringReader(codigoXml));
		if (inStream != null)
			return domBuilder.parse(inStream);
		return null;
	}
	
	/**
	 * Dar parse em uma String para XML removendo o DocType
	 * @param codigoXml
	 * @return Document
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 */
	public static Document parse(String codigoXml) throws ParserConfigurationException, SAXException, IOException {
		//Retirar sujeira ﻿<?xml
		int ini =codigoXml.indexOf("<?xml"); 
		if(ini>0){
			codigoXml = codigoXml.substring(ini);
		}
		
		codigoXml = retirarDocType(codigoXml);
		DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder domBuilder = domFactory.newDocumentBuilder();
		InputSource inStream = new InputSource();
		inStream.setCharacterStream(new StringReader(codigoXml));
		if (inStream != null)
			return domBuilder.parse(inStream);
		return null;
	}
	
	/**
	 * Remove o doctype
	 * @param file
	 * @return
	 */
	private static InputSource getXML(File file){
		StringBuilder buf = new StringBuilder();
		FileReader reader;
		try {
			reader = new FileReader(file);
			BufferedReader leitor = new BufferedReader(reader);
			while (leitor.ready()) {
				buf.append(leitor.readLine() + "\n");
			}
			leitor.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		//Retirar sujeira ﻿<?xml
		String limpar = buf.toString();
		int ini =limpar.indexOf("<?xml"); 
		if(ini>0){
			limpar = limpar.substring(ini);
		}
		String retorno = retirarDocType(limpar);
		InputSource inStream = new InputSource();
		inStream.setCharacterStream(new StringReader(retorno));
		return inStream;
	}
	
	private static InputSource getXML(String fullpath) {
		File file = new File(fullpath);
		if (!file.exists()) {
			return null;
		}
		return getXML(file);
	}
	
	/**
	 * Retira o doctype
	 * @param codXml codigo xml
	 * @return codXml sem doctype
	 */
	public static String retirarDocType(String codXml){
		String retorno = codXml;
		String retAux = retorno.toLowerCase();
		int aberto=1,fim=-1,ini = retAux.indexOf("<!doctype");
		
		if(ini==-1) return codXml;
		
		int i=ini+1;
		while(true){
			char c = retAux.charAt(i);
			char c2 = retAux.charAt(i+1);
			if(c=='<' && c2=='!'){
				aberto++;
			}
			if(c=='>'){
				aberto--;
			}
			if(aberto==0){
				fim = i+1;
				break;
			}
			i++;
		}
		if(ini>=codXml.length()){
			logger.error("ini is out of range");
		}
		if(fim>=codXml.length()){
			logger.error("fim is out of range");
		}
		String retornar = codXml.substring(0, ini) + codXml.substring(fim);
		return retornar;
	}
	
	/**
	 * Verifica se tem um indice,
	 * se houver busca no indice;
	 * @param id do elemento
	 * @return Node
	 */
	public Node getElementById(String id){
		if(indicePorId!=null){
			long start = System.currentTimeMillis();
			Node retorno = indicePorId.get(id);
			Xml.totalTempoById+=System.currentTimeMillis() - start;
			return retorno; 
		}else{
			return buscarTagId(this.dom,id);
		}
	}
	
	/**
	 * Cria um doc com o node passado como o root e entao faz a busca 
	 * implementada pelo proprio dom
	 * @param node
	 * @param tagName
	 * @return NodeList com os elementos que casam, clone
	 * @throws Exception
	 */
	public static NodeList getElementsByTagName(Node node, String tagName)throws Exception{
		DocumentBuilder domBuilder = domFactory.newDocumentBuilder();
		Document doc = domBuilder.newDocument();
		node = doc.importNode(node, true);
		doc.appendChild(node);
		return doc.getElementsByTagName(tagName);
	}
	
	/**
	 * Procura o elemento pelo id<br>
	 * Nao faz cache!
	 * @param doc
	 * @param id
	 * @return Node com o id especificado
	 */
	public static Node getElementById(Document doc,String id){
		return buscarTagId(doc.getFirstChild(),id);
	}
	/**
	 * Procura o elemento pelo id<br>
	 * Nao faz cache!
	 * @param node node pai
	 * @param id id a ser encontrado
	 * @return Node com o id especificado
	 */
	public static Node getElementById(Node node, String id) {
		long start = System.currentTimeMillis();
		Node retorno = buscarTagId(node,id); 
		totalTempoById += System.currentTimeMillis() - start;
		return retorno;
	}
	
	/**
	 * procura o Node com o id
	 */
	private static Node buscarTagId(Node xmlNode, String tagId) {
		NodeList nl = xmlNode.getChildNodes();
		int i, tot = nl.getLength();
		for (i = 0; i < tot; i++) {
			Node cn = nl.item(i);
			//Verificar o id do node atual
			if (cn.hasAttributes()) {
				NamedNodeMap at = cn.getAttributes();
				Node retorno = at.getNamedItem("id");
				if(retorno!=null && retorno.getNodeValue().equals(tagId)){
					return cn;
				}else{
					int j, tot2 = at.getLength();
					for (j = 0; j < tot2; j++) {
						if (at.item(j).getNodeName().toLowerCase().equals("id")) {
							String nodeId = at.item(j).getNodeValue().toString();
							if (nodeId.equals(tagId)) {
								return cn;
							}
						}
					}
				}
			}
			// busca nos Childs
			if (cn.hasChildNodes()) {
				Node res = buscarTagId(cn, tagId);
				if (res != null) {
					return res;
				}
			}
		}
		return null;
	}

	/**
	 * Retorna o atributo de uma tag
	 * @param node tag
	 * @param attName nome do atributo
	 * @return null ou o valor do atributo
	 */
	public static String getAtributo(Node node, String attName) {
		if(node==null) return null;
		if(!node.hasAttributes()) return null;
		Node att = node.getAttributes().getNamedItem(attName);
		if(att==null) return null;
		return att.getNodeValue();
	}

	/**
	 * Transforma um node em String
	 * @param node node para transformar
	 * @return String do node
	 */
	public static String nodeToString(Node node) {
		try{
			if(node==null) return "null";
			StringWriter sb = new StringWriter();
			OutputFormat of = new OutputFormat("XML", "ISO-8859-1", true);
			of.setIndent(1);
			of.setIndenting(true);
			XMLSerializer serializer = new XMLSerializer(sb, of);
			serializer.asDOMSerializer();
			serializer.serialize(node);
			return sb.toString().replaceAll("<\\?[^>]*>", "");
		}catch(Exception e){
			e.printStackTrace();
		}
		return null;
	}
	
	/**
	 * Retorna um atributo de uma tag
	 * @param tagInteira tag
	 * @param att atributo
	 * @return valor do atributo
	 */
	public static String getAtributo(String tagInteira, String att) {
		Pattern pattern = Pattern.compile("<.*?\\s" + att.toLowerCase() + "\\s*?=\\s*?([\"'])(.*?)\\1.*?>", Pattern.DOTALL);
		// Texto onde procurar o regex
		Matcher matcher = pattern.matcher(tagInteira.toLowerCase());
		if (matcher.find()) {
			return tagInteira.substring(matcher.start(2), matcher.end(2));
		}
		/*
		 * Verifica se existe
		 */
		if(tagInteira.toLowerCase().indexOf(att)==-1) return "";
		/*
		 * Testar quando nao possui aspas ou aspas simples
		 */
		int estado=0;
		if(tagInteira.endsWith("/>")){
			tagInteira = tagInteira.substring(0,tagInteira.length()-2);
		}else if(tagInteira.endsWith(">")){
			tagInteira = tagInteira.substring(0,tagInteira.length()-1);
		}
		String par[]=tagInteira.toLowerCase().split("\\s");
		for(int i=0;i<par.length;i++){
			if(estado==0){
				if(att.equals(par[i])){
					//verificar se o proximo eh um sinal de igual
					estado=1;
				}else{
					if(par[i].indexOf("=")!=-1){
						String attVal[]=par[i].split("=");
						try{
							if(attVal[0].equals(att)){
								return attVal[1]; 
							}
						}catch(Exception e){
							//System.out.println("att = " + att + " tagInteira='"+tagInteira+"'");
							return "";
						}
					}
				}
			}else if(estado==1){
				if(!par[i].equals("=") && !par[i].equals("")){
					return "";
				}
				estado=2;
			}else if(estado==2){
				if(!par[i].equals("")){
					return par[i];
				}
			}
		}
		return "";
	}
	
	/**
	 * Faz um indice para consultar os elementos por id
	 */
	public void indexarPorId(){
		long start = System.currentTimeMillis();
		indicePorId = new HashMap<String, Node>();
		indexarPorId(node);
		Xml.totalTempoById += System.currentTimeMillis()-start;
	}
	
	/**
	 * DFS
	 * @param node no para visitar
	 */
	private void indexarPorId(Node node){
		NodeList nodeList = node.getChildNodes();
		int tot = nodeList.getLength();
		for(int i=0;i<tot;i++){
			Node tmpNode = nodeList.item(i);
			if(tmpNode.getAttributes() !=null && 
				tmpNode.getAttributes().getNamedItem("id")!=null && 
					tmpNode.getAttributes().getNamedItem("id").getNodeValue() != null &&
					 !tmpNode.getAttributes().getNamedItem("id").getNodeValue().trim().isEmpty() )
			{
				indicePorId.put(tmpNode.getAttributes().getNamedItem("id").getNodeValue(), tmpNode);
			}
			indexarPorId(tmpNode);
		}
	}
	
	/**
	 * Facilitador para dom.getElementsByTagName()
	 * @param tagName nome da tag
	 * @return NodeList
	 */
	public NodeList getElementsByTagName(String tagName) {
		return dom.getElementsByTagName(tagName);
	}
	
	/**
	 * Procura uma tag com o valor especificado
	 * @param tagName nome da tag
	 * @param atributo nome do atributo
	 * @param valor valor do atributo
	 * @return Node ou null se nao achar
	 */
	public Node getElementByAtt(String tagName, String atributo, String valor) {
		NodeList list = getElementsByTagName(tagName);
		for(int i=0;i<list.getLength();i++){
			Node node = list.item(i);
			Node att = node.getAttributes().getNamedItem(atributo);
			if(att!=null && att.getNodeValue().equals(valor)){
				return node;
			}
		}
		return null;
	}
	
	
}